﻿/*

Title Cleaner
Rename scene release TV and movies.

Copyright 2013 Matthew Knox

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace TitleCleaner
{
    class Program
    {
        private static readonly List<string> Junk = new List<string>(new[] { "xvid", "hdtv", "uncut", "[vtv]", "dvdscr", "dvdrip", "rerip", "(", "repack", "xor", "proper", "notv", "uncut", "dvdrip", "xvid", "saints", "caph", "aph", "cam", "camrip", "ts", "telesync", "pdvd", "wp", "workprint", "tc", "telecine", "ppv", "ppvrip", "scr", "screener", "dvdscr", "dvdscreener", "bdscr", "ddc", "r5", "r5.line", "ac3", "hq", "dvdrip", "dvd-rip", "dvdr", "dvd-full", "full-rip", "iso rip", "lossless", "rip", "dvd-5", "dvd-9", "dsr", "dsrip", "dthrip", "dvbrip", "hdtv", "pdtv", "tvrip", "hdtvrip", "vodrip", "vodr", "bdrip", "brrip", "blu-ray", "bluray", "bdr", "bd5", "bd9", "bd25", "bd50", "web-rip", "webrip", "web rip", "webdl", "web-dl", "web dl", "h.264", "x264", "xvid", "divx", "r0", "r1", "r2", "r3", "r4", "r6", "r7", "r8", "r9" });

        public static void Main(string[] args)
        {
            var color = Console.ForegroundColor;
            try
            {
                if (args.Length >= 1)
                {
                    switch (args[0])
                    {
                        case "-test":
                            TestMain(args);
                            break;
                        case "-rename":
                            RenameMain(args);
                            break;
                        case "-normal":
                            NormalMain(args);
                            break;
                        default:
                            //case "-?":
                            {
                                Help();
                                return;
                            }
                    }
                }
                else
                {
                    NormalMain(args);
                }
            }
            catch(Exception)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("AN ERROR OCCURRED\nIf you used command line arguments, please check they are correct.");
            }
            Console.ForegroundColor = color;
        }

        private static void NormalMain(IList<string> args)
        {
            var directory = Environment.CurrentDirectory;
            var tv = true;
            switch (args.Count)
            {
                case 4:
                    {
                        LoadJunk(args[3]);
                        directory = ParseDirectory(args[2]);
                        tv = !args[1].Contains("movie");
                        break;
                    }
                case 3:
                    {
                        directory = ParseDirectory(args[2]);
                        tv = !args[1].Contains("movie");
                        break;
                    }
                case 2:
                    {
                        tv = !args[1].Contains("movie");
                        break;
                    }
            }

            Console.WriteLine("Reading from directory:\n" + directory);
            var fexts = new List<string>(new[]{"mov","mkv","flv","avi","mp4","mpg","vob"});
            var list = new List<string>(Directory.GetFiles(directory,"*.*",SearchOption.AllDirectories).Where(s => fexts.Any(s.EndsWith)));
    
            foreach (var t in list)
            {
                Console.Write("\n");
                var item = t.Substring(t.LastIndexOf(@"\", StringComparison.Ordinal)+1);
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine(item);
                var cleaned = (tv)?CleanTv(item):CleanMovie(item);
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine(cleaned);
                Console.ReadKey(true);
            }

            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("----");
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("Done.");
            Console.ReadKey(true);
        }

        private static void RenameMain(IList<string> args)
        {
            var directory = Environment.CurrentDirectory;
            var tv = true;
            switch (args.Count)
            {
                case 4:
                    {
                        LoadJunk(args[3]);
                        directory = ParseDirectory(args[2]);
                        tv = !args[1].Contains("movie");
                        break;
                    }
                case 3:
                    {
                        directory = ParseDirectory(args[2]);
                        tv = !args[1].Contains("movie");
                        break;
                    }
                case 2:
                    {
                        tv = !args[1].Contains("movie");
                        break;
                    }
            }

            Console.WriteLine("Reading from directory:\n" + directory);
            var fexts = new List<string>(new[] { "mov", "mkv", "flv", "avi", "mp4", "mpg", "vob" });
            var list = new List<string>(Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories).Where(s => fexts.Any(s.EndsWith)));

            foreach (var t in list)
            {
                Console.Write("\n");
                var item = t.Substring(t.LastIndexOf(@"\", StringComparison.Ordinal) + 1);
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine(item);
                var cleaned = (tv)?CleanTv(item):CleanMovie(item);
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine(cleaned);
                Console.WriteLine("Rename? [y/n]");
                var key = Console.ReadKey(false);
                if(key.Key == ConsoleKey.Y)
                {
                    File.Move(t, Environment.CurrentDirectory + @"\" + cleaned);
                }
            }

            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("----");
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("Done.");
            Console.ReadKey(true);
        }

        private static void TestMain(IList<string> args)
        {
            var filename = "tests.csv";
            var tv = true;
            switch (args.Count)
            {
                case 4:
                    {
                        LoadJunk(args[3]);
                        filename = args[2].Replace("-", "");
                        tv = !args[1].Contains("movie");
                        break;
                    }
                case 3:
                    {
                        filename = args[2].Replace("-", "");
                        tv = !args[1].Contains("movie");
                        break;
                    }
                case 2:
                    {
                        tv = !args[1].Contains("movie");
                        break;
                    }
            }

            Console.WriteLine("Reading from file: " + filename);
            var inp = new StreamReader(filename);
            var count = 0;
            var failed = 0;
            while (!inp.EndOfStream)
            {
                count++;
                var readLine = inp.ReadLine();
                if (readLine == null) continue;
                var testCase = readLine.Split(',');
                if (testCase.Length != 2) continue;
                testCase[0] = (tv)?CleanTv(testCase[0]):CleanMovie(testCase[0]);

                if (testCase[0] == testCase[1]) continue;
                failed++;
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("\nTest case " + count + " failed.");
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine(testCase[0] + "\n" + testCase[1]);
                Console.ReadKey(true);
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("Ignoring...");
            }
            inp.Close();

            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("----");
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("Correct:\t" + (count - failed).ToString(CultureInfo.InvariantCulture));
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("Failed:\t\t" + (failed).ToString(CultureInfo.InvariantCulture));
            Console.ReadKey(true);
        }

        private static string ParseDirectory(string arg)
        {
            try
            {
                if(arg.StartsWith("-directory="))
                {
                    arg = arg.Remove(0, 11);
                }
                if(!Directory.Exists(arg)) throw new Exception();
            }
            catch (Exception)
            {
                arg = Environment.CurrentDirectory;
                var color = Console.ForegroundColor;
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("Invalid Directory.");
                Console.ForegroundColor = color;
            }
            return arg;
        }

        private static void LoadJunk(string arg)
        {
            try
            {
                if (arg.StartsWith("-extraJunk=") || arg.StartsWith("-extrajunk="))
                {
                    arg = arg.Remove(0, 11);
                }
                if(!File.Exists(arg)) throw new Exception();
                var inj = new StreamReader(arg);
                {
                    while (!inj.EndOfStream)
                    {
                        var temp = inj.ReadLine();
                        AddToJunk(temp);
                    }
                    inj.Close();
                }
            }
            catch (Exception)
            {
                var color = Console.ForegroundColor;
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("Invalid JunkTitles file.");
                Console.ForegroundColor = color;
            }
        }

        private static void AddToJunk(string value)
        {
            value = value.ToLower();
            if(Junk.Contains(value)) return;
            Junk.Add(value);
        }

        private static void Help()
        {
            var color = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("TitleCleaner - Fix Scene Release Titles");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("Defaults to current directory, Normal Mode.\n");
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Command Line Switches:");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("\t-?\t\tThis help");
            Console.WriteLine("\t-test\t\tRun tests mode");
            Console.WriteLine("\t-rename\t\tRename mode");
            Console.WriteLine("\t-normal\t\tNormal mode [default]\n");
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Normal Mode");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("Uses the files from the current directory [default] and generates\nthe names that they would be renamed to BUT does not actually\nrename them.");
            Console.WriteLine("Example:");
            Console.ForegroundColor = color;
            Console.WriteLine("TitleCleaner.exe -normal [-type(tv/movie) [-directory=\"\" [-extraJunk=extraJunkTitles.txt]]]\n");
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Rename Mode");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("Same as for Normal Mode except actually renames files.\nDefaults to current directory.");
            Console.WriteLine("Example:");
            Console.ForegroundColor = color;
            Console.WriteLine("TitleCleaner.exe -rename [-type(tv/movie) [-directory=\"\" [-extraJunkTitles.txt]]]\n");
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Test Mode");
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("Takes a test input comma separated values file\n(default is \"tests.csv\") and then does the same as for\nNormal Mode, asking for confirmation at each incorrect value.");
            Console.WriteLine("Example:");
            Console.ForegroundColor = color;
            Console.WriteLine("TitleCleaner.exe -test [-type(tv/movie) [-tests.csv [-extraJunkTitles.txt]]]\n");
            Console.ForegroundColor = color;
        }

        private static readonly char[] Delim = { ',', '.', '_', ' ' };
        private static string CleanTv(string title)
        {
            // Split the episode title into manageable peices
            var splitted = new List<string>(title.Split(Delim));
            var fext = splitted[splitted.Count - 1];
            splitted.RemoveAt(splitted.Count - 1);
            
            // Remove Known Junk Names
            for (var i = 0; i < splitted.Count; i++)
            {
                // junk names greater than 3 in length can contain
                var i1 = i;
                if (Junk.Where(t => (splitted[i1].Length >= t.Length) && (t.Length > 3)).Any(t => splitted[i].Substring(0, t.Length).ToLower() == t))
                {
                    splitted.RemoveRange(i, splitted.Count - i);
                    break;
                }

                // junk names less than or equal to 3 in length must eqal
                var i2 = i;
                if (Junk.Where(t => (splitted[i2].Length >= t.Length) && (t.Length <= 3)).Any(t => splitted[i].ToLower() == t))
                {
                    splitted.RemoveRange(i, splitted.Count - i);
                    break;
                }

                // the square brackets rule
                if(Regex.Match(splitted[i],@"[\[][a-zA-Z]").Success)
                {
                    splitted.RemoveRange(i, splitted.Count - i);
                    break;
                }

                // the round brackets rule
                if (Regex.Match(splitted[i], @"[\(][a-zA-Z]").Success)
                {
                    splitted.RemoveRange(i, splitted.Count - i);
                    break;
                }

                // the year number rule
                if (Regex.Match(splitted[i], @"[0-9]{4}").Success && (Regex.Match(title, @"[sS][0-9]+[eE][0-9]+").Success || Regex.Match(title, @"[sS][0-9]+.*?[eE][0-9]+").Success))
                {
                    splitted.RemoveAt(i);
                }
            }

            // Autocapitalise first letter in each word
            for (var i = 0; i < splitted.Count; i++)
            {
                if (string.IsNullOrEmpty(splitted[i]))
                {
                    break;
                }
                var a = splitted[i].ToCharArray();
                a[0] = char.ToUpper(a[0]);
                splitted[i] = new string(a);
            }

            // Generate episode information block
            // Infinate loop escape variables:
            var curr = 0;
            var iter = 0;
            for (var i = 0; i < splitted.Count; i++)
            {
                if(curr == i && iter == 2) break;
                if(curr != i){curr = i; iter = 0;} else {iter++;}
                
                // some exceptional cases
                if (Regex.Match(splitted[i], @"[0-9]+[-][a-zA-Z].*").Success)
                {
                    splitted[i] = Regex.Replace(splitted[i], @"[-][a-zA-Z]", " ");
                    if(Regex.Match(splitted[i],@"[eE][0-9]+").Success)
                    {
                        splitted[i] = Regex.Replace(splitted[i], @"[ ]", "-");
                        splitted[i] = Regex.Replace(splitted[i], @"[sS]", " - [");
                        splitted[i] = Regex.Replace(splitted[i], @"[eE]", "x");
                    }
                    else
                    {
                        var spl = splitted[i].Split(' ');
#pragma warning disable 168
                        foreach (var t1 in from t1 in spl from t in Junk.Where(t => t.Length <= spl[1].Length &&
#pragma warning restore 168
                                                                                     spl[1].Substring(0, t.Length).ToLower() == t) select t1)
                        {
                            spl[1] = "";
                        }
                        splitted[i] = "";
                        foreach (var t in spl)
                        {
                            splitted[i] += t + " ";
                        }
                        splitted[i] = splitted[i].Trim();
                    }

                    splitted[i] = Regex.Replace(splitted[i], @"([sS]|[eE]|\[)", "");
                    var index = splitted[i].Length - ((int)(splitted[i].Length / 2.0)) * 2;
                    splitted[i] = " - [" + splitted[i].Insert(index, "x") + "]";
                    splitted[i] = Regex.Replace(splitted[i], @"((?<=[\[])(.*)(?=[0-9]+[x][0-9]+(-[0-9]+|)))", "");
                    splitted[i] = Regex.Replace(splitted[i], @"\d+", m => m.Value.PadLeft(2, '0'));
                    if (i + 1 != splitted.Count)
                    {
                        splitted[i] += " -";
                    }
                }

                // S00E00 || S0E0 || S00E0 || S0E00
                else if(Regex.Match(splitted[i],@"[sS][0-9]+[eE][0-9]+").Success)
                {
                    splitted[i] = Regex.Replace(splitted[i], "[sS]", " - [");
                    splitted[i] = Regex.Replace(splitted[i], "[eE]", "x");
                    splitted[i] += "]";
                    if(i+1 != splitted.Count)
                    {
                        splitted[i] += " -";
                    }
                    splitted[i] = Regex.Replace(splitted[i], @"\d+", m => m.Value.PadLeft(2, '0'));
                }

                // S00 && E00 || S0 && E0 || S00 & E0 || S0 && E00
                else if (Regex.Match(splitted[i], @"[sS][0-9]+").Success)
                {
                    for (var j = i+1; j < splitted.Count; j++)
                    {
                        if (!Regex.Match(splitted[j], @"[eE][0-9]+").Success) continue;
                        splitted[i] += splitted[j];
                        splitted[j] = "";
                        break;
                    }

                    splitted[i] = Regex.Replace(splitted[i], "[sS]", " - [");
                    splitted[i] = Regex.Replace(splitted[i], "[eE]", "x");
                    splitted[i] += "]";
                    if (i + 1 != splitted.Count)
                    {
                        splitted[i] += " -";
                    }
                    splitted[i] = Regex.Replace(splitted[i], @"\d+", m => m.Value.PadLeft(2, '0'));
                }
                
                // [00x0] || [0x00] || [0x0] || [00x00]
                else if(Regex.Match(splitted[i],@"\[[0-9]+[x][0-9]+\]").Success)
                {
                    splitted[i] = Regex.Replace(splitted[i], @"\d+", m => m.Value.PadLeft(2, '0'));
                    splitted[i] = " - " + splitted[i];
                    if (i + 1 != splitted.Count)
                    {
                        splitted[i] += " -";
                    }
                }

                // 00x0 || 0x00 || 0x0 || 00x00
                else if (Regex.Match(splitted[i], @"[0-9]+[x][0-9]+").Success)
                {
                    splitted[i] = "[" + splitted[i] + "]";
                    i--;
                }

                // 000+
                else if (Regex.Match(splitted[i], @"[0-9]+").Success && i != 0 && splitted[i].Length >= 3)
                {
                    var index = splitted[i].Length - ((int) (splitted[i].Length/2.0))*2;
                    splitted[i] = " - [" + splitted[i].Insert(index, "x") + "]";
                    splitted[i] = Regex.Replace(splitted[i], @"\d+", m => m.Value.PadLeft(2, '0'));
                    if (i + 1 != splitted.Count)
                    {
                        splitted[i] += " -";
                    }
                }
            }

            // Merge episode information block for extended episodes
            var ind = -1;
            for (var i = 0; i < splitted.Count; i++)
            {
                if (!Regex.Match(splitted[i], @".*?\[[0-9]+[x][0-9]+\].*").Success) continue;
                if(ind == -1) ind = i;
                else if(ind >= 0)
                {
                    splitted[i] = Regex.Replace(splitted[i], @".*?\[[0-9]+[x]", "");
                    splitted[i] = Regex.Replace(splitted[i], @"\]", "");
                    splitted[ind] = splitted[ind].Replace("]", "-" + splitted[i] + "]");
                    splitted[ind] = Regex.Replace(splitted[ind], @"\] -", "]");
                    splitted[i] = "";
                }
            }

            // Re-merge
            title = splitted.Aggregate("", (current, t) => current + (t + " "));

            // Append file extension
            title += "." + fext;
            title = Regex.Replace(title, @"([ ]|[-])+[.]"+fext, "."+fext);

            // Remove common duplicates
            title = Regex.Replace(title, @"[ ][ ]+", " ");
            title = Regex.Replace(title, @"[-]([ ][-])+", "-");
            title = Regex.Replace(title, @"[\[](\[)+", "[");
            title = Regex.Replace(title, @"[\]](\])+", "]");

            // Merge individual (alone) uppercase letters eg A M -> AM
            title = Regex.Replace(title, @"(?<=[A-Z])[ ](?=[A-Z][ .])", "");

            return title;
        }

        private static string CleanMovie(string title)
        {
            // Split the movie title into manageable peices
            var splitted = new List<string>(title.Split(Delim));
            var fext = splitted[splitted.Count - 1];
            splitted.RemoveAt(splitted.Count - 1);

            // Remove Known Junk Names
            for (var i = 0; i < splitted.Count; i++)
            {
                // junk names greater than 3 in length can contain
                var i1 = i;
                if (Junk.Where(t => (splitted[i1].Length >= t.Length) && (t.Length > 3)).Any(t => splitted[i].Substring(0, t.Length).ToLower() == t))
                {
                    splitted.RemoveRange(i, splitted.Count - i);
                    break;
                }

                // junk names less than or equal to 3 in length must eqal
                var i2 = i;
                if (Junk.Where(t => (splitted[i2].Length >= t.Length) && (t.Length <= 3)).Any(t => splitted[i].ToLower() == t))
                {
                    splitted.RemoveRange(i, splitted.Count - i);
                    break;
                }

                // the square brackets rule
                if (Regex.Match(splitted[i], @"[\[][a-zA-Z]").Success)
                {
                    splitted.RemoveRange(i, splitted.Count - i);
                    break;
                }

                // the round brackets rule
                if (Regex.Match(splitted[i], @"[\(][a-zA-Z]").Success)
                {
                    splitted.RemoveRange(i, splitted.Count - i);
                    break;
                }

                // the year number rule
                if (Regex.Match(splitted[i], @"[0-9]{4}").Success && (Regex.Match(title, @"[sS][0-9]+[eE][0-9]+").Success || Regex.Match(title, @"[sS][0-9]+.*?[eE][0-9]+").Success))
                {
                    splitted.RemoveAt(i);
                }
            }

            // Autocapitalise first letter in each word
            for (var i = 0; i < splitted.Count; i++)
            {
                if (string.IsNullOrEmpty(splitted[i]))
                {
                    break;
                }
                var a = splitted[i].ToCharArray();
                a[0] = char.ToUpper(a[0]);
                splitted[i] = new string(a);
            }

            // Re-merge
            title = splitted.Aggregate("", (current, t) => current + (t + " "));

            // Append file extension
            title += "." + fext;
            title = Regex.Replace(title, @"([ ]|[-])+[.]" + fext, "." + fext);

            // Remove common duplicates
            title = Regex.Replace(title, @"[ ][ ]+", " ");
            title = Regex.Replace(title, @"[-]([ ][-])+", "-");

            // Merge individual (alone) uppercase letters eg A M -> AM
            title = Regex.Replace(title, @"(?<=[A-Z])[ ](?=[A-Z][ .])", "");

            return title;
        }
    }
}
